/*
 * Copyright 2004-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package susano.datastore.manager;

import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;

/**
 * Datastore Manager
 * <p>
 * GAE/JのDatastoreServiceを利用するためのクラスです。
 * </p>
 *
 * @author tanaka.akira.2006
 * @version $Id: DatastoreManager.java 151 2009-10-09 09:35:18Z
 *          tanaka.akira.2006 $
 *
 */
public class DatastoreManager {

	protected static final Logger log = Logger.getLogger(DatastoreManager.class
			.getName());

	/** {@link DatastoreService} */
	public DatastoreService datastoreService;

	/** {@link Transaction} */
	public Transaction transaction;

	/** offset */
	public int offset = 0;

	/** limit */
	public int limit = 100;

	/** リトライ回数 */
	private static final int RETRY = 10;

	// =================================
	// public methods
	// =================================

	/**
	 * {@link Entity}を保存します。
	 *
	 * @param entity
	 * @return {@link Key}
	 */
	public Key put(Entity entity) {
		if (entity == null) {
			throw new NullPointerException("The entity parameter is null.");
		}
		init(true);
		for (int i = 0; i < RETRY; i++) {
			try {
				return datastoreService.put(entity);
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException("entity putting was failed!");
	}

	/**
	 * {@link Entity}リストを保存します。
	 *
	 * @param entities
	 *            {@link Iterable}
	 * @return {@link Key}リスト
	 */
	public List<Key> put(Iterable<Entity> entities) {
		if (entities == null) {
			throw new NullPointerException("The entities parameter is null.");
		}
		init(true);
		for (int i = 0; i < RETRY; i++) {
			try {
				return datastoreService.put(entities);
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException("entities putting was failed!");
	}

	/**
	 * 指定された１個以上の{@link Entity}を削除します。
	 *
	 * @param keys
	 */
	public void delete(Key... keys) {
		if (keys == null) {
			throw new NullPointerException("The keys parameter is null.");
		}
		init(true);
		for (int i = 0; i < RETRY; i++) {
			try {
				datastoreService.delete(keys);
				return;
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException(
				"entities deleting by keys was failed!");
	}

	/**
	 * 指定された１個以上の{@link Entity}を削除します。
	 *
	 * @param keys
	 */
	public void delete(Iterable<Key> keys) {
		if (keys == null) {
			throw new NullPointerException("The keys parameter is null.");
		}
		init(true);
		for (int i = 0; i < RETRY; i++) {
			try {
				datastoreService.delete(keys);
				return;
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException(
				"entities deleting by keys was failed!");
	}

	/**
	 * {@link Entity}を削除します。
	 *
	 * @param entity
	 */
	public void delete(Entity entity) {
		if (entity == null) {
			throw new NullPointerException("The entity parameter is null.");
		}
		init(true);
		for (int i = 0; i < RETRY; i++) {
			try {
				datastoreService.delete(entity.getKey());
				return;
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException("entity deleting was failed!");
	}

	/**
	 * {@link Entity}リストを削除します。
	 *
	 * @param entities
	 */
	public void delete(List<Entity> entities) {
		if (entities == null) {
			throw new NullPointerException("The entities parameter is null.");
		}
		init(true);
		for (Entity e : entities) {
			delete(e);
		}
	}

	/**
	 * １件の {@link Entity} を取得します。
	 *
	 * @param query
	 *            {@link Query}
	 * @return {@link Entity}
	 */
	public Entity asSingleEntity(Query query) {
		return prepare(query, false).asSingleEntity();
	}

	/**
	 * offsetを指定します。
	 *
	 * @param offset
	 * @return
	 */
	public DatastoreManager offset(int offset) {
		this.offset = offset;
		return this;
	}

	/**
	 * limitを指定します。
	 *
	 * @param limit
	 * @return
	 */
	public DatastoreManager limit(int limit) {
		this.limit = limit;
		return this;
	}

	/**
	 * 結果セットの{@link Entity}リストを取得します。
	 *
	 * @param query
	 *            {@link Query}
	 * @return {@link Entity}リスト
	 */
	public List<Entity> asList(Query query) {
		return prepare(query, false).asList(
				FetchOptions.Builder.withOffset(offset).limit(limit));
	}

	/**
	 * 結果セットの{@link Iterable}を取得します。
	 *
	 * @param query
	 *            {@link Query}
	 * @return {@link Iterable}
	 */
	public Iterable<Entity> asIterable(Query query) {
		return prepare(query, false).asIterable(
				FetchOptions.Builder.withOffset(offset).limit(limit));
	}

	/**
	 * 結果セットの{@link Iterator}を取得します。
	 *
	 * @param query
	 *            {@link Query}
	 * @return {@link Iterator}
	 */
	public Iterator<Entity> asIterator(Query query) {
		return prepare(query, false).asIterator(
				FetchOptions.Builder.withOffset(offset).limit(limit));
	}

	/**
	 * コミットします。
	 * <p>
	 * コミットに失敗すると内部で自動ロールバックを行い、false(コミット失敗)を返します。<br>
	 * この処理の最後に必ず transaction=null されます。
	 * </p>
	 *
	 * @return コミット成功: true / コミット失敗(ロールバックされた):false
	 */
	public boolean commit() {
		if (transaction == null) {
			throw new NullPointerException("transaction is null.");
		}
		for (int i = 0; i < RETRY; i++) {
			try {
				transaction.commit();
				if (transaction.isActive()) {
					rollback();
					log.log(Level.INFO, "tx=" + transaction.hashCode()
							+ " rollbacked");
					transaction = null;
					return false;
				} else {
					log.log(Level.INFO, "tx=" + transaction.hashCode()
							+ " committed");
					transaction = null;
					return true;
				}
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		transaction = null;
		throw new DatastoreTimeoutException("committing transaction failed!");
	}

	public void rollback() {
		if (transaction == null) {
			throw new NullPointerException("transaction is null.");
		}
		for (int i = 0; i < RETRY; i++) {
			try {
				transaction.rollback();
				return;
			} catch (DatastoreTimeoutException e) {
				log
						.log(Level.WARNING, "Retry(" + i + "): "
								+ e.getMessage(), e);
			}
		}
		throw new DatastoreTimeoutException("rollbacking transaction failed!");
	}

	/**
	 * 結果セットの件数を返します。
	 *
	 * @param query
	 *            {@link Query}
	 * @return 結果件数
	 */
	public int countEntities(Query query) {
		return prepare(query, false).countEntities();
	}

	// =================================
	// private methods
	// =================================

	private void init(final boolean transactionStart) {
		if (datastoreService == null) {
			datastoreService = DatastoreServiceFactory.getDatastoreService();
		}
		if (transaction == null && transactionStart) {
			transaction = datastoreService.beginTransaction();
			log.log(Level.INFO, "tx=" + transaction.hashCode() + " begin");
		}
	}

	private PreparedQuery prepare(Query query, final boolean transactionStart) {
		if (query == null) {
			return null;
		}
		init(transactionStart);
		return datastoreService.prepare(query);
	}

}
